/**************************************************************************
* This file is part of Hawkengine.
*
* Hawkengine is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Hawkengine is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Hawkengine.  If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/


/**************************************************************************
* Dependencies
**************************************************************************/
#include "engine/animation.h"
#include "engine/image.h"
#include "engine/render.h"
#include "engine/util.h"

/**************************************************************************
* Preprocessing
**************************************************************************/
#define FLASH_DELAY 1
#define CIRCLE_SECTIONS 32
#define LOG_BUFFER_SIZE 1024

/**************************************************************************
* Types
**************************************************************************/

/**************************************************************************
* Variables
**************************************************************************/
#if defined(USE_CUSTOM_ARRAY_BUFFERS) && !defined(NO_VERTEX_BUFFERS)
PFNGLGENBUFFERSARBPROC glGenBuffers = NULL;
PFNGLBINDBUFFERARBPROC glBindBuffer = NULL;
PFNGLBUFFERDATAARBPROC glBufferData = NULL;
PFNGLDELETEBUFFERSARBPROC glDeleteBuffers = NULL;
#endif

float render_modelview_matrix[16];
float render_projection_matrix[16];

UINT render_button_texture = 0;
UCHAR render_flash_alpha = 255;

extern const file_data_type button_png;
extern const file_data_type check_png;
extern const file_data_type check_bad_png;

/**************************************************************************
* Static Variables
**************************************************************************/
static const int VERTEX_DIMS = 3;
static const int NORMAL_DIMS = 3;
static const int TEXTURE_DIMS = 3;
static const int COLOR_DIMS = 4;
static const int MAX_DIMS = 4;

static char shown_message[MAX_MESSAGE_LENGTH];
static BOOL message_showing;
static message_task_node_type* message_tasks;
static message_task_node_type* unused_message_tasks;
static float* vertex_buffer = NULL;
static float* normal_buffer = NULL;
static float* texture_buffer = NULL;
static float* color_buffer = NULL;

static UINT vertex_vbo = 0;
static UINT normal_vbo = 0;
static UINT texture_vbo = 0;
static UINT color_vbo = 0;

static int circle_vertices_offset = 0;
static int unit_lines_vertices_offset = 0;
static int rect_vertices_offset = 0;
static int x_vertices_offset = 0;

static UINT check_texture[2];

/**************************************************************************
* Function Prototypes
**************************************************************************/
static void display_log(const bounds_type* bounds);
static void display_message(const bounds_type* bounds, const char* message);

/**************************************************************************
* Function Implementations
**************************************************************************/
static void display_log(const bounds_type* bounds)
{
  static char buffer[LOG_BUFFER_SIZE];
  static const color_type LOG_COLOR = {192, 128, 128, 128};
  static int last_size = -1;
  static int next_line_offset = 0;
  int size = 0;

  bounds_type shrunk_bounds;
  shrunk_bounds = (*bounds);
  bounds_shrink(&shrunk_bounds, 2.0);

  glPushMatrix();
  render_set_view_ortho(bounds);

  render_set_color(&LOG_COLOR);

  size = log_size();
  if (size > last_size) {
    // new data, read it again
    int start_idx = _MAX((size - LOG_BUFFER_SIZE), 0);
    int i;
    memset(&buffer, 0, sizeof(buffer));
    log_read(buffer, LOG_BUFFER_SIZE, start_idx);

    // find next line offset
    next_line_offset = 0;
    if (start_idx > 0) {
      for (i = 0; i < _MIN(LOG_BUFFER_SIZE, size); i++) {
        if (buffer[i] == '\n') {
          next_line_offset = (i + 1);
          break;
        }
      }
    }

    // trim off any partial lines
    for (i = (_MIN(LOG_BUFFER_SIZE, size) - 1); i >= 0; i--) {
      if (buffer[i] == '\n') {
        // last newline
        buffer[i] = 0;
        break;
      }
    }

    last_size = size;
  }


  font_render_text_wrapped(((char*)buffer) + next_line_offset, 10,
                           shrunk_bounds.left, shrunk_bounds.top,
                           bounds_width(&shrunk_bounds));

  glPopMatrix();
}

static void display_message(const bounds_type* bounds, const char* message)
{
  // setup displayed bounds
  bounds_type button_bounds;
  float border_size;

  glPushMatrix();
  render_set_view_ortho(bounds);

  border_size = (bounds_height(bounds) / 3.0);
  button_bounds = *bounds;
  button_bounds.top -= border_size;
  button_bounds.bottom += border_size;
  bounds_shrink(&button_bounds, 10.0);

  // render it
  render_button(&button_bounds, message, &OVERLAY_COLOR,
                &(ACTIVE_TEXT_COLORS[TRUE]));
  glPopMatrix();
}

int render_add_vertices(int count, float* vertices, float* normals,
                        float* textures, float* colors)
{
  static int size = 0;
  int float_size = sizeof(float);
  int offset = size;
  float* new_vertex_buffer = 0;
  float* new_normal_buffer = 0;
  float* new_texture_buffer = 0;
  float* new_color_buffer = 0;
  int new_size = (size + count);
  int old_buffer_size = (size * MAX_DIMS * float_size);
  int new_buffer_size = (new_size * MAX_DIMS * float_size);
  int i;

  // allocate new buffer space
  new_vertex_buffer = malloc(new_buffer_size);
  new_normal_buffer = malloc(new_buffer_size);
  new_texture_buffer = malloc(new_buffer_size);
  new_color_buffer = malloc(new_buffer_size);

  // initialize new memory
  memset(new_vertex_buffer, 0, new_buffer_size);
  memset(new_normal_buffer, 0, new_buffer_size);
  memset(new_texture_buffer, 0, new_buffer_size);
  memset(new_color_buffer, 0, new_buffer_size);

  // copy old data over
  if (size > 0) {
    memcpy(new_vertex_buffer, vertex_buffer, old_buffer_size);
    memcpy(new_normal_buffer, normal_buffer, old_buffer_size);
    memcpy(new_texture_buffer, texture_buffer, old_buffer_size);
    memcpy(new_color_buffer, color_buffer, old_buffer_size);
  }

  // free up old data
  free(vertex_buffer);
  free(normal_buffer);
  free(texture_buffer);
  free(color_buffer);

  // point to the new data
  vertex_buffer = new_vertex_buffer;
  normal_buffer = new_normal_buffer;
  texture_buffer = new_texture_buffer;
  color_buffer = new_color_buffer;

  // copy in new data
  for (i = 0; i < count; i++) {
    if (vertices) {
      memcpy(vertex_buffer + ((size + i) * MAX_DIMS),
             (vertices + (VERTEX_DIMS * i)), (VERTEX_DIMS * float_size));
    }
    if (normals) {
      memcpy(normal_buffer + ((size + i) * MAX_DIMS),
             (normals + (NORMAL_DIMS * i)), (NORMAL_DIMS * float_size));
    }
    if (textures) {
      memcpy(texture_buffer + ((size + i) * MAX_DIMS),
             (textures + (TEXTURE_DIMS * i)), (TEXTURE_DIMS * float_size));
    }
    if (colors) {
      memcpy(color_buffer + ((size + i) * MAX_DIMS),
             (colors + (COLOR_DIMS * i)), (COLOR_DIMS * float_size));
    }
  }

  size = new_size;

#ifndef NO_VERTEX_BUFFERS
  // delete any old vbos
  if (vertex_vbo) {
    glDeleteBuffers(1, &vertex_vbo);
    vertex_vbo = 0;
  }
  if (normal_vbo) {
    glDeleteBuffers(1, &normal_vbo);
    normal_vbo = 0;
  }
  if (texture_vbo) {
    glDeleteBuffers(1, &texture_vbo);
    texture_vbo = 0;
  }
  if (color_vbo) {
    glDeleteBuffers(1, &color_vbo);
    color_vbo = 0;
  }

  glGenBuffers(1, &vertex_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo);
  glBufferData(GL_ARRAY_BUFFER, new_buffer_size, vertex_buffer, GL_STATIC_DRAW);
  glGenBuffers(1, &normal_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, normal_vbo);
  glBufferData(GL_ARRAY_BUFFER, new_buffer_size, normal_buffer, GL_STATIC_DRAW);
  glGenBuffers(1, &texture_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, texture_vbo);
  glBufferData(GL_ARRAY_BUFFER, new_buffer_size, texture_buffer, GL_STATIC_DRAW);
  glGenBuffers(1, &color_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, color_vbo);
  glBufferData(GL_ARRAY_BUFFER, new_buffer_size, color_buffer, GL_STATIC_DRAW);
#endif

  return offset;
}

void render_arc(float radius, float radians, BOOL filled)
{
  int num_vertices = round_float(CIRCLE_SECTIONS * radians / (2 * PI)) + 1;
  glPushMatrix();
  glScalef(radius, radius, 1);
  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_NORMAL_ARRAY);
  if (filled) {
    glDrawArrays(GL_TRIANGLE_FAN, circle_vertices_offset, num_vertices + 1);
  } else {
    glDrawArrays(GL_LINE_LOOP, circle_vertices_offset + 1, num_vertices);
  }
  glDisableClientState(GL_VERTEX_ARRAY);
  glDisableClientState(GL_NORMAL_ARRAY);
  glPopMatrix();
}

void render_arc_thick(float radius, float radians, BOOL filled, float thickness)
{
  int num_vertices = round_float(CIRCLE_SECTIONS * radians / (2 * PI)) + 1;
  glPushMatrix();
  glScalef(radius, radius, thickness);
  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_NORMAL_ARRAY);
  if (filled) {
    glDrawArrays(GL_TRIANGLE_FAN, circle_vertices_offset, num_vertices + 1);
    glDrawArrays(GL_TRIANGLE_FAN, circle_vertices_offset + (CIRCLE_SECTIONS + 2),
                 num_vertices + 1);
    glDrawArrays(GL_TRIANGLE_STRIP,
                 circle_vertices_offset + 2 * (CIRCLE_SECTIONS + 2),
                 2 * num_vertices);
  } else {
    glDrawArrays(GL_LINE_STRIP, circle_vertices_offset + 1, num_vertices);
    glDrawArrays(GL_LINE_STRIP, circle_vertices_offset + (CIRCLE_SECTIONS + 2) + 1,
                 num_vertices);
    glDrawArrays(GL_LINES,
                 circle_vertices_offset + 2 * (CIRCLE_SECTIONS + 2),
                 2 * num_vertices);
  }
  glDisableClientState(GL_VERTEX_ARRAY);
  glDisableClientState(GL_NORMAL_ARRAY);
  glPopMatrix();
}

void render_button(const bounds_type* bounds, const char* text,
                   const color_type* bg_color, const color_type* fg_color)
{
  bounds_type text_bounds;
  float x_center, y_center;
  int num_lines, max_text_width;

  num_lines = font_count_lines(text, &max_text_width);

  glPushMatrix();
  bounds_center(bounds, &x_center, &y_center);
  glTranslatef(x_center, y_center, 0);
  glScalef(bounds_width(bounds), bounds_height(bounds), 1);

  // render the button background
  glEnableClientState(GL_VERTEX_ARRAY);
  if (bg_color && fg_color) {
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, render_button_texture);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    render_set_color(bg_color);
    glDrawArrays(GL_TRIANGLE_FAN, rect_vertices_offset, 4);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisable(GL_TEXTURE_2D);
  } else if (bg_color) {
    render_set_color(bg_color);
    glDrawArrays(GL_TRIANGLE_FAN, rect_vertices_offset, 4);
  } else if (!bg_color && fg_color) {
    render_set_color(fg_color);
    glDrawArrays(GL_LINE_LOOP, rect_vertices_offset, 4);
  }
  glDisableClientState(GL_VERTEX_ARRAY);
  glPopMatrix();

  // render the text
  if (max_text_width > 0) {
    text_bounds.left = (-max_text_width / 2.0) - 1;
    text_bounds.right = (max_text_width / 2.0) + 1;
    text_bounds.top = num_lines;
    text_bounds.bottom = -num_lines;
    bounds_keep_aspect(bounds, &text_bounds);
    bounds_center(&text_bounds, &x_center, &y_center);
    glPushMatrix();
    render_change_coordinates(bounds, &text_bounds);
    if (fg_color) {
      render_set_color(fg_color);
    }
    font_render_text(text, 2.0, x_center, y_center);
    glPopMatrix();
  }
}

BOOL render_capture(const bounds_type* bounds, const char* file_name)
{
  BOOL ok = FALSE;
  image_type image;
  UCHAR* file_data = NULL;
  FILE* file = NULL;
  char file_path[512];
  char file_extension[32];
  int file_size;

  memset(&image, 0, sizeof(image));
  image.width = (int)bounds_width(bounds);
  image.height = (int)bounds_height(bounds);
  image.channels = 3;
  image_calculate(&image, IMAGE_DATA_CHANNELS);

  glPixelStorei(GL_PACK_ALIGNMENT, image.channels);
  glReadPixels(bounds->left, bounds->bottom, image.width, image.height,
               GL_RGB, GL_UNSIGNED_BYTE, image.data[IMAGE_DATA_CHANNELS]);

  image_flip(&image, FALSE);

  image_resize(&image, (int)bounds_width(bounds), (int)bounds_height(bounds));

  get_data_dir(file_path);
  strcat(file_path, PATH_SEPARATOR);
  strcat(file_path, file_name);

  file = fopen(file_path, "wb");
  if (file) {
    if (file_get_extension(file_name, file_extension) &&
        image_write(&image, IMAGE_DATA_CHANNELS, file_extension,
                    &file_data, &file_size) &&
        (fwrite(file_data, file_size, 1, file) == 1)) {
      ok = TRUE;
    }
    fclose(file);
  }

  image_destroy(&image);
  free(file_data);

  return ok;
}

void render_change_coordinates(const bounds_type* old_bounds,
                               const bounds_type* new_bounds)
{
  if ((new_bounds->right <= new_bounds->left) ||
      (new_bounds->top <= new_bounds->bottom)) {
    return;
  }

  glTranslatef(old_bounds->left, old_bounds->bottom, 0);
  glScalef((bounds_width(old_bounds) / bounds_width(new_bounds)),
           (bounds_height(old_bounds) / bounds_height(new_bounds)), 1);
  glTranslatef(-new_bounds->left, -new_bounds->bottom, 0);
}

void render_check(const bounds_type* bounds, BOOL ok, BOOL colored)
{
  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, check_texture[ok]);
  glEnableClientState(GL_TEXTURE_COORD_ARRAY);
  if (colored) {
    if (ok) {
      render_set_color(&GREEN);
    } else {
      render_set_color(&RED);
    }
  }
  render_rect(bounds, TRUE);
  glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  glDisable(GL_TEXTURE_2D);
}

void render_circle(float radius, BOOL filled)
{
  render_arc(radius, 2 * PI, filled);
}

void render_circle_thick(float radius, BOOL filled, float thickness)
{
  render_arc_thick(radius, 2 * PI, filled, thickness);
}

void render_dark_shade(const bounds_type* bounds, UCHAR alpha)
{
  color_type color = BLACK;
  color.a = alpha;
  render_set_color(&color);
  render_rect(bounds, TRUE);
}

void render_dismiss_message(void)
{
  animation_cancel(&message_showing);
  message_showing = FALSE;
}

float render_get_depth_at(float x, float y, const bounds_type* bounds)
{
#ifdef OPENGL_ES
  UNUSED_PARAM(x);
  UNUSED_PARAM(y);
  UNUSED_PARAM(bounds);
  return 0;
#else
  int viewport[4];
  float viewport_loc[2];
  float depth;

  glGetIntegerv(GL_VIEWPORT, viewport);

  viewport_loc[X] = (int)change_scale(x, bounds->left, bounds->right,
                                      viewport[0], viewport[0] + viewport[2]);
  viewport_loc[Y] = (int)change_scale(y, bounds->bottom, bounds->top,
                                      viewport[1], viewport[1] + viewport[3]);
  depth = 0;
  glReadPixels(viewport_loc[X], viewport_loc[Y], 1, 1,
               GL_DEPTH_COMPONENT, GL_FLOAT, &depth);

  return depth;
#endif
}

void render_init(void)
{
  static const float MAT_SPECULAR_COLOR[] = {1, 1, 1, 1};
  static const float MAT_SHININESS_COLOR[] = {0.7};
  static const float UNIT_LINES_VERTICES[][3] = {
    {-0.5, 0, 0}, {0.5, 0, 0}, {0, -0.5, 0}, {0, 0.5, 0}, {0, 0, -0.5}, {0, 0, 0.5},
  };
  static const float RECT_VERTICES[][3] = {
    // bottom
    {-0.5,-0.5, 0}, {-0.5, 0.5, 0}, { 0.5, 0.5, 0}, { 0.5,-0.5, 0},
    // top
    {-0.5,-0.5, 1}, { 0.5,-0.5, 1}, { 0.5, 0.5, 1}, {-0.5, 0.5, 1},
    // left
    {-0.5, 0.5, 0}, {-0.5,-0.5, 0}, {-0.5,-0.5, 1}, {-0.5, 0.5, 1},
    // right
    { 0.5, 0.5, 0}, { 0.5, 0.5, 1}, { 0.5,-0.5, 1}, { 0.5,-0.5, 0},
    // front
    {-0.5,-0.5, 0}, { 0.5,-0.5, 0}, { 0.5,-0.5, 1}, {-0.5,-0.5, 1},
    // back
    {-0.5, 0.5, 0}, {-0.5, 0.5, 1}, { 0.5, 0.5, 1}, { 0.5, 0.5, 0},
  };
  static const float RECT_NORMALS[][3] = {
    // bottom
    {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1},
    // top
    {0, 0, 1}, {0, 0, 1}, {0, 0, 1}, {0, 0, 1},
    // left
    {-1, 0, 0}, {-1, 0, 0}, {-1, 0, 0}, {-1, 0, 0},
    // right
    {1, 0, 0}, {1, 0, 0}, {1, 0, 0}, {1, 0, 0},
    // front
    {0, -1, 0}, {0, -1, 0}, {0, -1, 0}, {0, -1, 0},
    // back
    {0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0},
  };
  static const float RECT_TEXTURES[][3] = {
    // bottom
    {0, 0, 0}, {0, 1, 0}, {1, 1, 0}, {1, 0, 0},
    // top
    {0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1},
    // left
    {0, 1, 0}, {0, 0, 0}, {0, 0, 1}, {0, 1, 1},
    // right
    {1, 1, 0}, {1, 1, 1}, {1, 0, 1}, {1, 0, 0},
    // front
    {0, 0, 0}, {1, 0, 0}, {1, 0, 1}, {0, 0, 1},
    // back
    {0, 1, 0}, {0, 1, 1}, {1, 1, 1}, {1, 1, 0},
  };
  static const float X_VERTICES[][3] = {
    {-0.5, -0.5, 0}, {0.5, 0.5, 0}, {-0.5, 0.5, 0}, {0.5, -0.5, 0},
  };
  float circle_vertices[4 * (CIRCLE_SECTIONS+1) + 2][3];
  float circle_normals[4 * (CIRCLE_SECTIONS+1) + 2][3];
  int i, vertex_idx;
  float theta;

  memcpy(&render_modelview_matrix, &MATRIX_IDENTITY_4, sizeof(render_modelview_matrix));
  memcpy(&render_projection_matrix, &MATRIX_IDENTITY_4, sizeof(render_projection_matrix));

#if defined(USE_CUSTOM_ARRAY_BUFFERS) && !defined(NO_VERTEX_BUFFERS)
  glGenBuffers = (PFNGLGENBUFFERSARBPROC)wglGetProcAddress("glGenBuffersARB");
  glBindBuffer = (PFNGLBINDBUFFERARBPROC)wglGetProcAddress("glBindBufferARB");
  glBufferData = (PFNGLBUFFERDATAARBPROC)wglGetProcAddress("glBufferDataARB");
  glDeleteBuffers = (PFNGLDELETEBUFFERSARBPROC)wglGetProcAddress("glDeleteBuffersARB");
#endif

  memset(&shown_message, 0, sizeof(shown_message));
  unused_message_tasks = NULL;
  message_tasks = NULL;
  message_showing = FALSE;

  memset(circle_vertices, 0, sizeof(circle_vertices));
  memset(circle_normals, 0, sizeof(circle_normals));

  vertex_idx = 0;

  // setup bottom vertices
  circle_normals[vertex_idx][Z] = -1;
  vertex_idx++;
  theta = 0;
  for (i = 0; i < (CIRCLE_SECTIONS + 1); i++) {
    circle_vertices[vertex_idx][X] = cos(theta);
    circle_vertices[vertex_idx][Y] = sin(theta);
    circle_normals[vertex_idx][X] = circle_vertices[vertex_idx][X];
    circle_normals[vertex_idx][Y] = circle_vertices[vertex_idx][Y];
    circle_normals[vertex_idx][Z] = -1.4;
    theta += (2.0 * PI / ((float)CIRCLE_SECTIONS));
    vertex_idx++;
  }

  // setup top vertices
  circle_vertices[vertex_idx][Z] = 1;
  circle_normals[vertex_idx][Z] = 1;
  vertex_idx++;
  theta = 0;
  for (i = 0; i < (CIRCLE_SECTIONS + 1); i++) {
    circle_vertices[vertex_idx][X] = cos(theta);
    circle_vertices[vertex_idx][Y] = sin(theta);
    circle_vertices[vertex_idx][Z] = 1;
    circle_normals[vertex_idx][X] = circle_vertices[vertex_idx][X];
    circle_normals[vertex_idx][Y] = circle_vertices[vertex_idx][Y];
    circle_normals[vertex_idx][Z] = 1.4;
    theta += (2.0 * PI / ((float)CIRCLE_SECTIONS));
    vertex_idx++;
  }

  // setup sides
  theta = 0;
  for (i = 0; i < (CIRCLE_SECTIONS + 1); i++) {
    circle_vertices[vertex_idx][X] = cos(theta);
    circle_vertices[vertex_idx][Y] = sin(theta);
    circle_normals[vertex_idx][X] = circle_vertices[vertex_idx][X];
    circle_normals[vertex_idx][Y] = circle_vertices[vertex_idx][Y];
    vertex_idx++;

    circle_vertices[vertex_idx][X] = circle_vertices[vertex_idx - 1][X];
    circle_vertices[vertex_idx][Y] = circle_vertices[vertex_idx - 1][Y];
    circle_vertices[vertex_idx][Z] = 1;
    circle_normals[vertex_idx][X] = circle_vertices[vertex_idx][X];
    circle_normals[vertex_idx][Y] = circle_vertices[vertex_idx][Y];
    vertex_idx++;

    theta += (2.0 * PI / ((float)CIRCLE_SECTIONS));
  }

  rect_vertices_offset = render_add_vertices(24, (float*)RECT_VERTICES,
                                             (float*)RECT_NORMALS,
                                             (float*)RECT_TEXTURES, NULL);
  unit_lines_vertices_offset = render_add_vertices(6,
                                                   (float*)UNIT_LINES_VERTICES,
                                                   NULL, NULL, NULL);
  x_vertices_offset = render_add_vertices(4, (float*)X_VERTICES,
                                          NULL, NULL, NULL);
  circle_vertices_offset = render_add_vertices(4 * (CIRCLE_SECTIONS + 1) + 2,
                                               (float*)circle_vertices,
                                               (float*)circle_normals, NULL, NULL);

  // setup textures
  render_button_texture = image_texture_from_data(&button_png);
  check_texture[TRUE] = image_texture_from_data(&check_png);
  check_texture[FALSE] = image_texture_from_data(&check_bad_png);

  // clear black
  glClearColor(0, 0, 0, 1.0);

  // set opengl parameters
  glShadeModel(GL_SMOOTH);
  glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

  glDepthMask(TRUE);
  glDepthFunc(GL_LEQUAL);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  // lighting
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, MAT_SPECULAR_COLOR);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, MAT_SHININESS_COLOR);
}

void render_line(int dimensions, const float* p1, const float* p2)
{
  float midpoint[3];
  float scale = get_distance(p1, p2, dimensions);
  float proj_scale = get_distance(p1, p2, 2);
  int i;

  _UPDATE_MIN(dimensions, 3);

  memset(midpoint, 0, sizeof(midpoint));
  for (i = 0; i < dimensions; i++) {
    midpoint[i] = ((p1[i] + p2[i]) / 2.0);
  }

  glPushMatrix();
  glTranslatef(midpoint[X], midpoint[Y], midpoint[Z]);
  glScalef(scale, scale, scale);
  glRotatef(atan2(p2[Y] - p1[Y], p2[X] - p1[X]) * DEG_PER_RAD, 0, 0, 1);
  if (dimensions == 3) {
    glRotatef(atan2(p2[Z] - p1[Z], proj_scale) * DEG_PER_RAD, 0, -1, 0);
  }
  render_unit_line(X);
  glPopMatrix();
}

void render_look_at(const float eye[3], const float look_at[3], const float up[3])
{
  static float f[3], s[3], u[3], matrix[16];
  int i;

  // calculate viewing vector
  for (i = 0; i < 3; i++) {
    f[i] = (look_at[i] - eye[i]);
    u[i] = up[i];
  }

  // normalize vectors
  normalize(f);
  normalize(u);

  cross_product(f, u, s);
  cross_product(s, f, u);

  // create full transformation matrix (column major)
  memset(matrix, 0, sizeof(matrix));
  for (i = 0; i < 3; i++) {
    matrix[4 * i] = s[i];
    matrix[4 * i + 1] = u[i];
    matrix[4 * i + 2] = -f[i];
  }
  matrix[15] = 1;

  glMultMatrixf(matrix);
  glTranslatef(-eye[0], -eye[1], -eye[2]);
}

void render_perspective(float fovy, float aspect, float zNear, float zFar)
{
  float xmin, xmax, ymin, ymax;
  ymax = zNear * tan(fovy * PI / 360.0);
  ymin = -ymax;
  xmin = ymin * aspect;
  xmax = ymax * aspect;
#ifdef OPENGL_ES
  glFrustumf(
#else
  glFrustum(
#endif
    xmin, xmax, ymin, ymax, zNear, zFar);
}

BOOL render_project(float x, float y, float z,
                    const bounds_type* bounds,
                    const float model[16], const float projection[16],
                    float* win_x, float* win_y, float* win_z)
{
  float in[4], out[4];

  in[X] = x;
  in[Y] = y;
  in[Z] = z;
  in[3] = 1.0;
  matrix_multiply_44_41(model, in, out);
  matrix_multiply_44_41(projection, out, in);

  if (in[3] == 0.0) {
    return FALSE;
  }

  in[X] /= in[3];
  in[Y] /= in[3];
  in[Z] /= in[3];

  *win_x = change_scale(in[X], -1, 1, bounds->left, bounds->right);
  *win_y = change_scale(in[Y], -1, 1, bounds->bottom, bounds->top);
  *win_z = change_scale(in[Z], -1, 1, 0, 1);

  return TRUE;
}

void render_rect(const bounds_type* bounds, BOOL filled)
{
  glPushMatrix();
  render_change_coordinates(bounds, &UNIT_BOUNDS);

  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_NORMAL_ARRAY);
  if (filled) {
    glDrawArrays(GL_TRIANGLE_FAN, rect_vertices_offset, 4);
  } else {
    glDrawArrays(GL_LINE_LOOP, rect_vertices_offset, 4);
  }
  glDisableClientState(GL_VERTEX_ARRAY);
  glDisableClientState(GL_NORMAL_ARRAY);

  glPopMatrix();
}

void render_rect_thick(const bounds_type* bounds, BOOL filled, float thickness)
{
  int i;

  glPushMatrix();
  render_change_coordinates(bounds, &UNIT_BOUNDS);
  glScalef(1, 1, thickness);

  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_NORMAL_ARRAY);
  for (i = 0; i < 6; i++) {
    if (filled) {
      glDrawArrays(GL_TRIANGLE_FAN, rect_vertices_offset + (i * 4), 4);
    } else {
      glDrawArrays(GL_LINE_LOOP, rect_vertices_offset + (i * 4), 4);
    }
  }
  glDisableClientState(GL_VERTEX_ARRAY);
  glDisableClientState(GL_NORMAL_ARRAY);

  glPopMatrix();
}

void render_set_color(const color_type* color)
{
  float fcolor[4] = {
    (color->r / 255.0), (color->g / 255.0), (color->b / 255.0), (color->a / 255.0)
  };
  glColor4f(fcolor[0], fcolor[1], fcolor[2], fcolor[3]);
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, fcolor);
}

void render_set_viewport(const bounds_type* bounds)
{
  // set environment
  glViewport((int)bounds->left, (int)bounds->bottom,
             (int)(bounds->right - bounds->left),
             (int)(bounds->top - bounds->bottom));
}

void render_set_view_ortho(const bounds_type* bounds)
{
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
#ifdef OPENGL_ES
  glOrthof(
#else
  glOrtho(
#endif
    bounds->left, bounds->right,
    bounds->bottom, bounds->top,
  -1, 1);
  glMatrixMode(GL_MODELVIEW);

  glDisable(GL_DEPTH_TEST);
  glDisable(GL_LIGHTING);
}

void render_set_view_perspective(const float eye[3],
                                 const float look_at[3],
                                 const float up[3],
                                 float fov_min,
                                 float aspect_ratio,
                                 float near_plane,
                                 float far_plane)
{
  static float gl_matrix[16];
  float fov_y;

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  if ((aspect_ratio > 0) && (aspect_ratio < 1)) {
    // taller than it is wide
    fov_y = (2.0 * DEG_PER_RAD * atan((tan(fov_min * RAD_PER_DEG / 2.0)) /
                                      aspect_ratio));
  } else {
    // wider, use min as y fov
    fov_y = fov_min;
  }

  render_perspective(fov_y, aspect_ratio, near_plane, far_plane);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  render_look_at(eye, look_at, up);

  glEnable(GL_DEPTH_TEST);
  glEnable(GL_LIGHTING);

  // opengl produces column major, transpose it to row major
  glGetFloatv(GL_MODELVIEW_MATRIX, gl_matrix);
  matrix_transpose(gl_matrix, 4, 4, render_modelview_matrix);
  glGetFloatv(GL_PROJECTION_MATRIX, gl_matrix);
  matrix_transpose(gl_matrix, 4, 4, render_projection_matrix);
}

void render_set_view_perspective_camera(const camera_type* camera,
                                        float fov_min,
                                        float aspect_ratio,
                                        float near_plane,
                                        float far_plane)
{
  render_set_view_perspective(camera->pos, camera->focus, camera->up,
                              fov_min, aspect_ratio, near_plane, far_plane);
}

void render_show_message(const char* message, float duration)
{
  message_task_node_type* new_node = NULL;
  if (unused_message_tasks) {
    // use existing node
    new_node = unused_message_tasks;
    unused_message_tasks = unused_message_tasks->next;
  } else {
    // no ready-made unused tasks, create one
    new_node = (message_task_node_type*)malloc(sizeof(message_task_node_type));
  }
  strcpy(new_node->message_task.message, message);
  new_node->message_task.duration = duration;
  new_node->next = NULL;
  if (!message_tasks) {
    // first node on the queue
    message_tasks = new_node;
  } else {
    // find last on queue
    message_task_node_type* temp_node = message_tasks;
    while (temp_node->next) {
      temp_node = temp_node->next;
    }
    temp_node->next = new_node;
  }
}

BOOL render_showing_message(void)
{
  return (message_showing);
}

void render_start(const bounds_type* bounds)
{
  UNUSED_PARAM(bounds);
  int stride = (MAX_DIMS * sizeof(float));

  // attach all vertex buffers
  if (vertex_vbo) {
#ifndef NO_VERTEX_BUFFERS
    glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo);
    glVertexPointer(VERTEX_DIMS, GL_FLOAT, stride, NULL);
#endif
  } else {
    glVertexPointer(VERTEX_DIMS, GL_FLOAT, stride, vertex_buffer);
  }
  if (normal_vbo) {
#ifndef NO_VERTEX_BUFFERS
    glBindBuffer(GL_ARRAY_BUFFER, normal_vbo);
    glNormalPointer(GL_FLOAT, stride, NULL);
#endif
  } else {
    glNormalPointer(GL_FLOAT, stride, normal_buffer);
  }
  if (texture_vbo) {
#ifndef NO_VERTEX_BUFFERS
    glBindBuffer(GL_ARRAY_BUFFER, texture_vbo);
    glTexCoordPointer(TEXTURE_DIMS, GL_FLOAT, stride, NULL);
#endif
  } else {
    glTexCoordPointer(TEXTURE_DIMS, GL_FLOAT, stride, texture_buffer);
  }
  if (color_vbo) {
#ifndef NO_VERTEX_BUFFERS
    glBindBuffer(GL_ARRAY_BUFFER, color_vbo);
    glColorPointer(COLOR_DIMS, GL_FLOAT, stride, NULL);
#endif
  } else {
    glColorPointer(COLOR_DIMS, GL_FLOAT, stride, color_buffer);
  }

  // clear anything in the buffer
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

void render_stop(const bounds_type* bounds)
{
  if (render_showing_message()) {
    // pending message
    display_message(bounds, shown_message);
  }

  if (debugging_level >= MAX_DEBUGGING_LEVEL) {
    // current log
    display_log(bounds);
  }

  glFlush();
}

void render_texture(const bounds_type* bounds, UINT texture)
{
  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_TEXTURE_COORD_ARRAY);
  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, texture);
  render_rect(bounds, TRUE);
  glDisable(GL_TEXTURE_2D);
  glDisableClientState(GL_VERTEX_ARRAY);
  glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}

void render_uninit(void)
{
  message_task_node_type* node;

  while (message_tasks) {
    node = message_tasks;
    message_tasks = message_tasks->next;
    free(node);
  }
  message_tasks = NULL;

  while (unused_message_tasks) {
    node = unused_message_tasks;
    unused_message_tasks = unused_message_tasks->next;
    free(node);
  }
  unused_message_tasks = NULL;
}

void render_unit_line(int dimension)
{
  if ((dimension < X) || (dimension > Z)) {
    return;
  }
  glEnableClientState(GL_VERTEX_ARRAY);
  glDrawArrays(GL_LINES, unit_lines_vertices_offset + (2 * dimension), 2);
  glDisableClientState(GL_VERTEX_ARRAY);
}

BOOL render_unproject(float x, float y, float z,
                      const bounds_type* bounds,
                      const float model[16], const float projection[16],
                      float* view_x, float* view_y, float* view_z)
{
  float m[16], A[16];
  float in[4], out[4];

  // put x,y,z between -1 and 1
  in[X] = change_scale(x, bounds->left, bounds->right, -1, 1);
  in[Y] = change_scale(y, bounds->bottom, bounds->top, -1, 1);
  in[Z] = change_scale(z, 0, 1, -1, 1);
  in[3] = 1.0;

  // multiply
  matrix_multiply_44_44(projection, model, A);
  matrix_invert_4(A, m);
  matrix_multiply_44_41(m, in, out);
  if (out[3] == 0.0)
    return FALSE;

  *view_x = out[X] / out[3];
  *view_y = out[Y] / out[3];
  *view_z = out[Z] / out[3];
  return TRUE;
}

void render_update(float secs)
{
  static float flash_secs = 0;

  if (!render_showing_message() && message_tasks) {
    // pending message and last one has to be done animating to be here
    // go ahead and show next message
    message_task_node_type* processed_node = message_tasks;
    strcpy(shown_message, message_tasks->message_task.message);
    message_showing = TRUE;
    if (message_tasks->message_task.duration > 0) {
      // automatically turn off after delay
      animation_set_int(&message_showing, FALSE,
                        message_tasks->message_task.duration);
    }
    message_tasks = message_tasks->next;

    // delete old node by moving to a ready-queue
    processed_node->next = unused_message_tasks;
    unused_message_tasks = processed_node;
  }

  // update flashing
  flash_secs += secs;
  if (flash_secs >= FLASH_DELAY) {
    flash_secs = 0;
  }

  render_flash_alpha = (UCHAR)((int)(255.0 * sin(flash_secs / FLASH_DELAY * PI)));
}

void render_x(const bounds_type* bounds)
{
  glPushMatrix();
  render_change_coordinates(bounds, &UNIT_BOUNDS);

  glEnableClientState(GL_VERTEX_ARRAY);
  glDrawArrays(GL_LINES, x_vertices_offset, 4);
  glDisableClientState(GL_VERTEX_ARRAY);

  glPopMatrix();
}
